package epam.concurrency.lab2.task3;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * My implementation of connection pool
 *
 * @author Iurii Miedviediev
 * @version 1.0 Build 14.08.2014
 */
public class ConnectionPool {

    //Constants
    private static final int MAX_CONNECTIONS = 10;
    private static final int WAITING_TIMEOUT = 3000;
    private static final int IDLE_TIMEOUT = 4000;
    private static final String DRIVER = "org.h2.Driver";
    private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPool.class);

    //semaphore for limiting the number of connections used simultaneously
    private static final Semaphore SEM = new Semaphore(MAX_CONNECTIONS, true);

    //holders for connections
    private Queue<Connection> availableConns = new ConcurrentLinkedQueue<Connection>();
    private Map<Connection, Long> usedConns = new ConcurrentHashMap<>();

    Thread inspector;
    String url;
    String user;
    String passwd;

    /**
     * Standard constructor for custom Connection pool
     * @param url address of the database
     * @param user login
     * @param passwd password
     */
    public ConnectionPool(String url, String user, String passwd) {
        this.url = url;
        this.user = user;
        this.passwd = passwd;

        try {
            Class.forName(DRIVER).newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            LOGGER.error("Could not create connection pool", e);
        }
    }

    /**
     * Launch connection pool
     */
    public void startConnPool() {
        for (int i = 0; i < MAX_CONNECTIONS; i++) {
            //get Connection to the DB and wrap it into custom Connection wrapper
            availableConns.add(new PoolConnection(createConnection()));
        }

        //launch daemon thread to regularly inspect used connections
        inspector = new Thread(new Inspector());
        inspector.setDaemon(true);
        inspector.start();
    }

    /**
     * Get Connection to the specified DB
     * @return Connection to the DB on given URL
     */
    public Connection createConnection() {
        try {
            return DriverManager.getConnection(url, user, passwd);
        } catch (SQLException e) {
            LOGGER.error("Failed to create connection", e);
            return null;
        }
    }

    /**
     * Get a connection from the pool
     * @return Connection (wrapped in a custom wrapper) to the required DB
     * @throws SQLException
     * @throws InterruptedException
     * @throws TimeoutException in case all connections are acquired for a long time 
     */
    public synchronized Connection checkout() throws TimeoutException {
        try {
            if(!SEM.tryAcquire(WAITING_TIMEOUT, TimeUnit.MILLISECONDS)) {
                throw new TimeoutException();
            }
            Connection newConn = availableConns.poll();

            //remember the time connection was taken
            usedConns.put(newConn, System.currentTimeMillis());
            return newConn;
        } catch (InterruptedException e) {
            LOGGER.error("Failed to checkout the Connection", e);
        }
        return null;
    }

    /**
     * Return connection back to the pool
     * @param c Connection to return
     */
    public synchronized void checkin(Connection c) {
        if (usedConns.remove(c) != null) {
            availableConns.add(c);
            SEM.release();
        }
    }

    /**
     * Close all connections of the current pool and clear both connection holders
     * @throws SQLException
     */
    public void shutdown() {
        for(Connection c : usedConns.keySet()) {
            try {
                c.close();
            } catch (SQLException e) {
                LOGGER.error("Failed to shut down the pool", e);
            }
        }
        for(Connection c : availableConns) {
            //unwrap the fair Connection to the DB and close it
            Connection con = ((PoolConnection) c).con;
            if(con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    LOGGER.error("Failed to shut down the pool", e);
                }
            }
        }
        usedConns.clear();
        availableConns.clear();
    }

    /**
     * Custom wrapper for the Connections
     * Resets the time when Connection was taken from the pool
     * in case of invocation of any method 
     *
     * Instead of closing the Connection, pushes it back to the pool
     */
    private class PoolConnection extends AbstractConnectionWrapper {

        public PoolConnection(Connection con) {
            super(con);
        }

        /**
         * Used every time any Connection method (except for close() and isClosed()) called
         * Reset the timer of the current connection's usage in the pool
         */
        @Override
        public void resetTimer() {
            usedConns.put(this, System.currentTimeMillis());
        }

        /**
         * Instead of closing the connection, return it back to the pool
         */
        @Override
        public void close() throws SQLException {
            checkin(this);
        }

        /**
         * Returns true if current Connection is available for use, false otherwise
         */
        @Override
        public boolean isClosed() {
            return !usedConns.containsKey(this);
        }
    }

    /**
     * Special runnable for inspecting the timeout of used connections
     * If connection is not used for the IDLE_TIMEOUT, is is pushed to the pool automatically
     */
    private class Inspector implements Runnable {
        @Override
        public void run() {
            while(true) {
                for(Connection c : usedConns.keySet()) {
                    Long time = usedConns.get(c);
                    if(time != null && System.currentTimeMillis() - time >= IDLE_TIMEOUT) {
                        checkin(c);
                    }
                }
                try {
                    Thread.sleep(IDLE_TIMEOUT);
                } catch (InterruptedException e) {
                    LOGGER.error("Failed while going to sleep (inspector thread)", e);
                }
            }
        }
    }
}